JavaでCloudFrontの署名付きURL (signed URL) を生成する
よく訓練されたアップル信者、都元です。CloudFrontでコンテンツ保護を行う場合、サーバサイドで署名付きURLを発行する必要があります。この仕組みについて詳しくは弊社佐々木のエントリーCloudFront+S3で署名付きURLでプライベートコンテンツを配信するを御覧ください。
さて、上記のエントリでは主にPerlのスクリプトを用いて、作業用のローカルマシン上で署名付きURLを生成する手順をご紹介しています。また、参照先としてご紹介したドキュメントでは、下記のように各プログラミング言語上で署名付きURLを生成する方法について説明があります。
このうちJavaの解説ではJetS3t *1というAPIラッパーライブラリを用いていますが、現在の標準APIラッパーはAWS SDK for Javaです。従って、署名を生成するためだけに、わざわざJetS3tを採用する理由は特に無い *2と思います。
また、このドキュメントのコードはBouncy Castleという、これも古くからあるJavaの暗号実装に依存しています。暗号の実装は、J2SE 5.0(要するにJava5)以降であれば Java SE Security として標準提供されていますので、本稿ではそちらを使いたいと思います。
サンプルコード
というわけで、もっとシンプルに署名付きURLを生成するコードをご紹介します。このコードは「引数として下記のような情報を受け取り、標準出力に署名付きURLを出力する」という、シンプルなコマンドラインツールとして記述しました。
一応Java7で動作確認しています。出来れば外部のライブラリに依存せずに実現したかったのですが、1箇所だけGuavaを使わせてもらいました。Java8であれば、Guavaを使わずに標準APIだけで書けます。
import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.SecureRandom; import java.security.Signature; import java.security.spec.PKCS8EncodedKeySpec; import com.google.common.io.BaseEncoding; public class SignedURLGenerator { private static final long DEFAULT_DURATION_SEC = 60 * 5; // 5 minutes private static final String POLICY_FORMAT = "{\"Statement\":[{\"Resource\":\"%s\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":%d}}}]}"; private static final String URL_FORMAT = "%s%sExpires=%d&Signature=%s&Key-Pair-Id=%s"; public static void main(String[] args) throws Exception { if (args.length != 3 && args.length != 4) { System.out.printf("usage: java %s <baseUrl> <keyPairId> <keyPath> [durationSeconds]%n", SignedURLGenerator.class.getName()); System.out.println(" baseUrl ... Resource URL to sign. ex. http://xxx.cloudfront.net/path/to/resource"); System.out.println(" keyPairId ... CloudFront key pair ID. ex. APKAZZZZZZZZZZZZZZZZ"); System.out.println(" keyPath ... Path to private key. ex. /path/to/privateKey.der"); System.out.println(" durationSeconds ... Expiring duration in seconds. (optional, default 300) ex. 60"); System.exit(1); } String resourceUrl = args[0]; String keyPairId = args[1]; Path privateKeyFile = Paths.get(args[2]); long durationSeconds = args.length == 4 ? Long.parseLong(args[3]) : DEFAULT_DURATION_SEC; byte[] derPrivateKey = Files.readAllBytes(privateKeyFile); long expires = (System.currentTimeMillis() / 1000) + durationSeconds; String signedUrl = generateSignedURL(resourceUrl, keyPairId, derPrivateKey, expires); System.out.println(signedUrl); } private static String generateSignedURL(String resourceUrl, String keyPairId, byte[] derPrivateKey, long expires) { String policy = String.format(POLICY_FORMAT, resourceUrl, expires); try { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(derPrivateKey); PrivateKey privateKey = keyFactory.generatePrivate(privSpec); // Sign data Signature signature = Signature.getInstance("SHA1withRSA"); signature.initSign(privateKey, new SecureRandom()); signature.update(policy.getBytes("UTF-8")); byte[] signatureBytes = signature.sign(); String signatureString = BaseEncoding.base64().encode(signatureBytes); // Convert the given data to be safe for use in signed URLs for a private distribution by // using specialized Base64 encoding. String urlSafeSignature = signatureString .replace('+', '-') .replace('=', '_') .replace('/', '~'); return String.format(URL_FORMAT, resourceUrl, resourceUrl.contains("?") ? "&" : "?", expires, urlSafeSignature, keyPairId); } catch (Exception e) { throw new Error(e); } } }
コード詳解
非常にシンプルなので一気にご紹介します。
- 1〜9行目
- 非常にシンプルなimportです。Java標準クラスと、Guavaにしか依存していないことが分かります。
- 13〜18行目
- 各種定数の定義です。デフォルト値と書式文字列ですね。
- 21〜42行目
- mainメソッドです。ヘルプ表示や引数の解析等、本稿としてはあまり本質的ではない部分かと思います。
- 44行目
- 本稿のメインgenerateSignedURLメソッドです。
- 45行目
- まず、このURLが指し示すリソースに対してどのようなアクセス制限が必要なのか、それを表現するJSON文字列を生成します。定数書式に従って、ですね。ここを拡張すれば、IP縛り等のアクセス制限も実装可能です。
- 48〜57行目
- Java SE SecurityのAPIを利用して、電子署名を行っている部分です。署名はバイト列としてsignatureBytesに得られます。
- 58行目
- Guavaを使った箇所。署名バイト列をBase64エンコードしています。Java8には標準実装がありますね。それ以前であればこのコードのようにGuavaを使うか、またはcommons-codec等をご利用ください。
- 60〜65行目
- Base64エンコードした文字列をそのままURLに指定してしまうと、URLとして+, =, /が特殊解釈されてしまうため、これらをそれぞれ-, _, ~に置換します。この置換はドキュメントにも明記されています。
- 67〜72行目
- ここまでで得られた署名等の情報を元に、URLを組み立てて返します。
まとめ
というわけで、JetS3tに依存しない CloudFront 署名付きURL生成方法をご紹介しました。